#include "Arduino.h"
#ifdef __cplusplus
extern "C" void __cxa_pure_virtual(void) { while(1); }
#endif

/*
 Arduino 5 Button LCD Display Menu
 By Joe (grugly at sdf dot org)
 https://notabug.org/grugly/Arduino_5_Button_16x2_LCD_Menu
*/

// this needs to be here for some reason
#ifdef __cplusplus
extern "C" {
#endif

// Include LCD and sensor libraries
#include "LiquidCrystal.h"
#include "DHT.h"

// Pin assignment
#define LCD_PIN_1	8
#define LCD_PIN_2	9
#define LCD_PIN_3	4
#define LCD_PIN_4	5
#define LCD_PIN_5	6
#define LCD_PIN_6	7
#define BACKLIGHT_PIN	10
#define BUTTON_PIN	A0
#define FAN_PIN		11
#define LAMP_PIN	12
#define SENSOR_PIN	13

// Button reference voltage
#define BUTTON_NULL	1023
#define BUTTON_SELECT	639
#define BUTTON_LEFT	408
#define BUTTON_DOWN	255
#define BUTTON_UP	98
#define BUTTON_RIGHT	0

// Init LCD and sensor
LiquidCrystal lcd(LCD_PIN_1, LCD_PIN_2, LCD_PIN_3,
		  LCD_PIN_4, LCD_PIN_5, LCD_PIN_6);
DHT dht;

// Defaults
int backlight	= 0;		// Holds backlight status
int brightness	= 128;		// Brightnes of backlight
int sleep_time	= 10000;	// Time before screen turns off
int refresh_delay = 5000;	// Info display refresh delay
int cycle_delay	= 2000;		// Work cycle refresh delay
int temp_target	= 35;		// Optimum temp for our enclosure
int temp_variance = 5;		// How far off the temp range before we correct

// Variables to hold current data
int temp	= 0;		// Temperature
int temp_high	= 0;		// Highest temp
int temp_low	= 99;		// Lowest temp
int humid	= 0;		// Humidity
int humid_high	= 0;		// Highest humidity
int humid_low	= 99;		// Lowest humidity
String outstr	= "";		// String for LCD output lines
int fan_state	= 0;		// 1 when fan is running
int lamp_state	= 0;		// 1 when lamp is on

unsigned long fan_time, fan_duration, fan_average,
		lamp_time, lamp_duration, lamp_average;
int fan_times, lamp_times = 0;

// Timers
unsigned long run_duration, wake_time, wake_duration,
				last_refresh, last_cycle = 0;

// Displayed text for each menu item (16 char max)
String menu_list[] = {
	"Target Temp",
	"Temp Variance",
	"Brightness",
	"Standby Delay",
	"Uptime",
	"Fan Time",
	"Fan Average",
	"Lamp Time",
	"Lamp Average"
};

// Count and store number of menu items
int items = round((sizeof(menu_list) / sizeof(String) - 1));
int item = 0;		// Menu position
int menu_displayed = 0;	// Menu displayed if 1

// Custom chars for LCD for welcome screen
byte chicken0[8] = { // High bounce
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110,
	B00100,
	B00000
};
byte chicken1[8] = { // Low bounce
	B00000,
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110,
	B00000
};
byte chicken2[8] = { // Squatting to lay
	B00000,
	B00000,
	B00000,
	B00000,
	B01100,
	B11100,
	B01111,
	B01110
};
byte chicken3[8] = { // Laying the egg
	B00000,
	B00000,
	B01000,
	B00001,
	B01100,
	B11100,
	B01111,
	B01110
};
byte egg[8] = { // The egg
	B00000,
	B00000,
	B00000,
	B00000,
	B00100,
	B01110,
	B01110,
	B00100
};

// Welcome message with a chicken laying an egg
void welcome() {
	lcd.clear();
	lcd.setCursor(0, 0);
	lcd.print("Chicken Brooder");
	int i;
	int anichar = 0;	// Make chicken bounce
	for (i = 16; i >= 0; i--) {
		lcd.setCursor((i + 1), 1);
		lcd.print(" ");
		lcd.setCursor(i, 1);
		lcd.write(anichar);
		if (i == 8) { // Lay egg at char 8
			lcd.setCursor(i, 1);
			lcd.write(2);
			delay(500);
			lcd.setCursor(i, 1);
			lcd.write(3);
			delay(500);
			lcd.setCursor((i - 1), 1);
			lcd.write(anichar);
			lcd.write(4);
			i--;
		}
		// Bounce chicken
		if (anichar == 0) { anichar++; }
		else { anichar--; }
		delay(500);
	}
}

void readTemp() {
	// Read temperature
	temp = round(dht.getTemperature());
	if (temp > temp_high) { temp_high = temp; }
	if (temp < temp_low)  { temp_low = temp; }
	return;
}

void readHumid() {
	// Read humidity
	humid = round(dht.getHumidity());
	if (humid > humid_high) { humid_high = humid; }
	if (humid < humid_low)  { humid_low = humid; }
	return;
}

void printTemp() {
	// Display temperature
	lcd.clear();
	lcd.setCursor(0, 0);
	outstr = "Temperature: " + String(temp) + 'C';
	lcd.print(outstr);
	lcd.setCursor(0, 1);
	outstr = "L: " + String(temp_low) + "C    H: " + String(temp_high) + "C";
	lcd.print(outstr);
	return;
}

void printHumid() {
	// Display humidity
	lcd.clear();
	lcd.setCursor(0, 0);
	outstr = "Humidity:    " + String(humid) + '%';
	lcd.print(outstr);
	lcd.setCursor(0, 1);
	outstr = "L: " + String(humid_low) + "%    H: " + String(humid_high) + "%";
	lcd.print(outstr);
	return;
}

// Display statistics
int disp_sw = 0;
void displayInfo() {
	if ((run_duration - last_refresh) >= refresh_delay) {
		if (disp_sw == 0) {
			printTemp();
			disp_sw++;
		}
		else {
			printHumid();
			disp_sw--;
		}
		// Reset timer
		last_refresh = millis();
	}
}

// Read sensors, flick switches, etc..
void doWork() {
	if ((run_duration - last_cycle) >= cycle_delay) {
		// Get the data
		readTemp();
		readHumid();

		// Fan control
		if (temp >= (temp_target + temp_variance) && fan_state == 0) {
			digitalWrite(FAN_PIN, HIGH);
			fan_state = 1;
			fan_time = millis();
			fan_times++;
		}
		if (temp <= temp_target && fan_state == 1) {
			digitalWrite(FAN_PIN, LOW);
			fan_state = 0;
			fan_duration += (run_duration - fan_time);
			fan_average = (fan_duration / fan_times);
		}

		// Lamp control
		if (temp <= (temp_target - temp_variance) && lamp_state == 0) { 
			digitalWrite(LAMP_PIN, HIGH); 
			lamp_state = 1;
			lamp_time = millis();
			lamp_times++;
		}
		if (temp >= temp_target && lamp_state == 1) { 
			digitalWrite(LAMP_PIN, LOW); 
			lamp_state = 0;
			lamp_duration += (run_duration - lamp_time);
			lamp_average = (lamp_duration / lamp_times);
		}
	}
}

// Read a button press and return number of button pressed
int readButton() {
	int button_voltage = analogRead(BUTTON_PIN);
	int button_pressed = 0;

	if (button_voltage <= (BUTTON_NULL + 10) 
	 && button_voltage >= (BUTTON_NULL - 10)) {
		button_pressed = 0;
	}
	if (button_voltage <= (BUTTON_SELECT + 10)
	 && button_voltage >= (BUTTON_SELECT - 10)) {
		button_pressed = 1;
	}
	if (button_voltage <= (BUTTON_LEFT + 10)
	 && button_voltage >= (BUTTON_LEFT - 10)) {
		button_pressed = 2;
	}
	if (button_voltage <= (BUTTON_DOWN + 10)
	 && button_voltage >= (BUTTON_DOWN - 10)) {
		button_pressed = 3;
	}
	if (button_voltage <= (BUTTON_UP + 10)
	 && button_voltage >= (BUTTON_UP - 10)) {
		button_pressed = 4;
	}
	if (button_voltage <= (BUTTON_RIGHT + 10)
	 && button_voltage >= (BUTTON_RIGHT - 10)) {
		button_pressed = 5;
	}

	// Wait until button is released before we return
	while (button_voltage <= (BUTTON_NULL - 10)) {
		button_voltage = analogRead(BUTTON_PIN);
		delay(100);
	}

	if (button_pressed > 0) {
		wake_time = millis();	// Reset the backlight timer
	}

	return button_pressed;
}

// Display the menu item on the LCD
void displayItem(int item) {
	// Clear the screen and print the item title
	lcd.clear();
	lcd.print(String(menu_list[item]));

	// Display the current value in a readable form
	// Compiler says these need defining outside the case statement!
	int percent, sec;
	unsigned long d, h, m, s;
	switch (item) {
		case 0: // Target temperature
			lcd.setCursor(0, 1);
			lcd.print(String(temp_target) + "C");
			break;
		case 1: // Temperature variance
			lcd.setCursor(0, 1);
			lcd.print(String(temp_variance) + "C");
			break;
		case 2: // Brightness is displayed as a percentage
			// We don't want to turn the screen completely off
			// and a value of 256 will overflow it so we trim
			// the values. I like the result.
			percent = round(((brightness - 16) / 224.0) * 100);
			lcd.setCursor(0, 1);
			lcd.print(String(percent) + "%");
			break;
		case 3: // Stand-by delay is shown in seconds
			if (sleep_time < 21000) {
				sec = sleep_time / 1000;
				lcd.setCursor(0, 1);
				lcd.print(String(sec) + "sec");
			}
			else {
				lcd.setCursor(0, 1);
				lcd.print("OFF");
			}
			break;
		case 4: // Display uptime
			s = (run_duration / 1000) % 60;
			m = (run_duration / 60000) % 60;
			h = (run_duration / 3600000) % 24;
			d = (run_duration / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
		case 5: // Display fan time
			s = (fan_duration / 1000) % 60;
			m = (fan_duration / 60000) % 60;
			h = (fan_duration / 3600000) % 24;
			d = (fan_duration / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
		case 6: // Display fan average time
			s = (fan_average / 1000) % 60;
			m = (fan_average / 60000) % 60;
			h = (fan_average / 3600000) % 24;
			d = (fan_average / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
		case 7: // Display lamp time
			s = (lamp_duration / 1000) % 60;
			m = (lamp_duration / 60000) % 60;
			h = (lamp_duration / 3600000) % 24;
			d = (lamp_duration / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
		case 8: // Display lamp average time
			s = (lamp_average / 1000) % 60;
			m = (lamp_average / 60000) % 60;
			h = (lamp_average / 3600000) % 24;
			d = (lamp_average / 86400000) % 12;
			lcd.setCursor(0, 1);
			lcd.print(String(d) + "d " + String(h) + "h " + String(m) + "m " + String(s) + "s ");
			break;
	}
}

// Change values of menu items
// Action is 0 for decreasing (left button) and
// 1 for increasing (right button)
void updateItem(int item, int action) {
	switch (item) {
		case 0: // Target Temperature 
			// Minimum temperature is 20C
			if (action == 0 && temp_target > 20) {
				temp_target--;
			}
			// Max temperature is 40C 
			else if (action == 1 && temp_target < 40) {
				temp_target++;
			}
			break;
		case 1: // Temperature Variance
			// Minimum temperature variance is 1C
			if (action == 0 && temp_variance > 1) {
				temp_variance--;
			}
			// Max temperature variance is 10C 
			else if (action == 1 && temp_variance < 10) {
				temp_variance++;
			}
			break;
		case 2: // Brightness
			// don't dim the screen to off
			if (action == 0 && brightness != 16) {
				brightness = brightness - 16;
				analogWrite(BACKLIGHT_PIN, brightness);
			}
			// 256 overflows and the screen is quite bright anyway
			else if (action == 1 && brightness != 240) {
				brightness = brightness + 16;
				analogWrite(BACKLIGHT_PIN, brightness);
			}
			break;
		case 3: // Standby Delay
			// Minimum delay is 2 seconds
			if (action == 0 && sleep_time > 2000) {
				sleep_time -= 1000;
			}
			// Max is 20 seconds (21 is off)
			else if (action == 1 && sleep_time < 21000) {
				sleep_time += 1000;
			}
			break;
		// To display info, we don't need to modify the value
		// so for item[2] (Uptime) we don't take any action.
	}
	displayItem(item);
}

void setup(void) {
	// Setup LCD
	lcd.begin(16, 2);
	lcd.noCursor();
	lcd.createChar(0, chicken0);	// Create jumping chicken char
	lcd.createChar(1, chicken1);	// Create landing chicken char
	lcd.createChar(2, chicken2);	// Create squatting chicken char
	lcd.createChar(3, chicken3);	// Create laying chicken char
	lcd.createChar(4, egg);		// Create egg char

	// Setup sensor
	dht.setup(SENSOR_PIN);

	// Setup button and backlight pins
	pinMode(BUTTON_PIN, INPUT);
	pinMode(BACKLIGHT_PIN, OUTPUT);
	pinMode(LAMP_PIN, OUTPUT);
	pinMode(FAN_PIN, OUTPUT);

	// Turn backlight on
	analogWrite(BACKLIGHT_PIN, brightness);
	backlight = 1;

	welcome();
	wake_time = millis();
	readTemp();

	return;
}

void loop(void) {
	// Note how long we have been running for
	run_duration = millis();
	// and how long since the last button push
	wake_duration = run_duration - wake_time;

	// Hide the menu/go to sleep
	if (backlight == 1 && wake_duration >= sleep_time) {
		if (menu_displayed == 1) {
			menu_displayed = 0;
			item = 0;
			lcd.clear();
			wake_time = millis();
		} else if (sleep_time != 21000) {
			digitalWrite(BACKLIGHT_PIN, LOW);
			lcd.clear();
			backlight = 0;
			wake_time = 0;
		}
	}

	// Read a button press
	int button = readButton();

	// Wake up when the select button is pushed
	if (button == 1 && backlight == 0) {
		last_refresh = 0;
		analogWrite(BACKLIGHT_PIN, brightness);
		backlight = 1;
	}

	// Show the menu while awake and the select button is pushed
	else if (button == 1 && menu_displayed == 0) {
		// Set menu flag
		menu_displayed = 1;

		// Display the first item
		displayItem(item);
	}

	else if (button >= 1 && menu_displayed == 1) {
		switch (button) {
			case 1:	// Select button pushed
				// Exit menu
				menu_displayed = 0;
				item = 0;
				lcd.clear();
				displayInfo();
				return;
				break;
			case 2:	// Left button pushed
				// Decrease value
				updateItem(item, 0);
				break;
			case 3:	// Down button pushed
				// Display next menu item
				if (++item > items) { item = 0; }
				displayItem(item);
				break;
			case 4:	// Up button pushed
				// Display previous menu item
				if (--item < 0) { item = items; }
				displayItem(item);
				break;
			case 5:	// Right button pushed
				// Increase value
				updateItem(item, 1);
				break;
		}
	}

	// Show info while awake
	else if (backlight == 1 && menu_displayed == 0) {
		displayInfo();
	}

	// Always do something usefull..
	doWork();

	return;
}

#ifdef __cplusplus
}
#endif
#include <Arduino.h>

int main(void)
{
	init();

#if defined(USBCON)
	USBDevice.attach();
#endif
	
	setup();
    
	for (;;) {
		loop();
		if (serialEventRun) serialEventRun();
	}
        
	return 0;
}

